FreeBSD 上的 UPS 工具 NUT
NUT 是 C/S 架构的软件。它的优点在于省成本,不用买很贵的带网络管理卡的 UPS,只需要一个 master 节点能和 UPS 通信就够了。断电时候 master 节点可以通过网络通知 slave 节点关机。
它的缺点是,如果停电后、来电前的时间间隔较短,UPS 没有自动关闭,那么,来电后,所有节点启动时,要先等 master 节点启动 (server 所在机器),然后再在 master 节点上使用 wake-on-lan 唤醒其他 slave 节点。而如果停电后、来电前的时间间隔较长,UPS 自动关闭,那么,来电后 UPS 重新开机也可以让所有连接到该 UPS 的主机自动开机。不必先等 master 节点启动,再由 master 节点唤醒其他节点。
NUT 在 FreeBSD 上有 4 个 daemon:
- upsd(8) 是 server side 的程序。它和 NUT client 使用 TCP 通信,也负责与 UPS 驱动交互。但它只负责将信号转发到 NUT client ,不负责操作主机
- upsmon(8) 是 client side 的程序,负责操作主机以执行用户定义的操作
- upssched(8) 用来根据触发条件执行用户自定义的操作,比如脚本之类
- upslog(8) 定期轮询 UPS,获取相关信息,将它们格式化后写入文件 (写日志)
NUT 的配置文件被放在 /usr/local/etc/nut 下,主要要用到的有:
- nut.conf (影响 upsd) 被用于配置 NUT 的工作模式
- ups.conf (影响 upsd) 被用于配置 NUT 和 UPS 的连接。也可用于重载 NUT 某些默认的配置,比如:
battery.charge.low
表示触发自动关机时的 UPS 电量battery.runtime.low
表示出发自动关机时的设备可运行时间
- upsd.conf (影响 upsd) 当一个 UPS 被用来保护多台机器时,这个文件被用于配置 slave 访问 master 的方式
- upsd.users (影响 upsd) 被用于配置访问 upsd 进程的角色
- upsmon.conf (影响 upsmon) 被用于配置 NUT 要监视哪个 UPS,在什么时候执行关机操作
- upssched.conf (影响 upssched) 中可以配置用户自定义的操作,比如脚本之类,并配置触发条件
man [ nut.conf | ups.conf | upsd.conf | upsd.users | upsmon.conf | upssched.conf ]
可查看详细信息。
配置目标:
- 两台主机 A 和 B 都由 UPS 供电,其中:
- A 既是 NUT server 也是 NUT client,通过 USB 直接连接 UPS 并接入网络。(NUT server 即 upsd 进程只管通信,关机操作由 NUT client 即 upsmon 进程完成,所以 A 也需要一个 client 进程来完成关机操作)
- B 作为 NUT client,接入网络并由 A 控制关机。
- 主机 A 上的 upsd 屏蔽 UPS 主动发来的低电量状态信号。因为有些 UPS 检测到市电输入消失就会立即发送低电量信号,而 NUT 检测到低电量信号就会触发关机操作。所以为了避免这个问题,直接把 UPS 发来的低电量信号屏蔽掉,由 NUT 自己判断什么时候触发低电量状态。
- 主机 A 上不使用 剩余最大可运行时间
battery.runtime.low
进行关机判定。这个值不很准。 - UPS 剩余电量低于 60% 自动关闭 A B 主机。
UPS 设备为 APC BK650M2-CH,它使用 USB 与主机通信。以下操作在 FreeBSD 中完成,NUT 软件的版本为 v2.8.0。
另外,FreeBSD 所在的硬件为 HPE Microserver Gen10,该配置可以正常关闭系统,但来电之后 UPS 无法通知到 Gen10,所以 Gen10 无法在来电之后自动启动。
如果 UPS 支持 switched output outlets 功能的话,可以让 UPS 在来电时有一个断开插座的电源供应再恢复的功能,这个功能可以让 Gen10 在来电时自动启动系统。(需要在 BIOS 里将电源状态调整为 'always power on')
群晖的家用硬件能获取从 USB 发来的电源供应恢复的信号并开机是因为,它在主板上外接了额外的电路来通过 USB 监控 UPS 状态,这个额外的电路可以从连接的 UPS 的 USB 线取电,所以才能保证在关机状态下也能获取 UPS 信息并通知系统开机。
那么最终实现来电自动开机的方案是,在没有接入 UPS 的 RouterOS 路由器上写一个脚本每 10 分钟执行一次。脚本内 ping Gen10,没有回应就使用 WoL 唤醒 Gen10,这个方案也能正常工作。
根据配置目标,A 主机上需要修改的文件有:
- nut.conf
- ups.conf
- upsd.conf
- upsd.users
- upsmon.conf
用 RJ45 转 USB 线连接 UPS 和 A 机器上之后,先在 A 机器上用 tail /var/log/messages
可以看到已与 UPS 建立连接,输出类似这样:
Jun 19 21:12:23 gen10 kernel: ugen0.2: <American Power Conversion Back-UPS BK650M2-CH FW:294803G -292804G> at usbus0 Jun 19 21:14:25 gen10 kernel: ugen0.2: <American Power Conversion Back-UPS BK650M2-CH FW:294803G -292804G> at usbus0 (disconnected) Jun 19 21:14:26 gen10 kernel: ugen0.2: <American Power Conversion Back-UPS BK650M2-CH FW:294803G -292804G> at usbus0
或者执行 usbconfig
可以看到这样的输出:
ugen0.1: <AMD XHCI root HUB> at usbus0, cfg=0 md=HOST spd=SUPER (5.0Gbps) pwr=SAVE (0mA) ugen1.1: <AMD EHCI root HUB> at usbus1, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=SAVE (0mA) ugen0.2: <American Power Conversion Back-UPS BK650M2-CH FW:294803G -292804G> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA) ugen1.2: <vendor 0x0438 product 0x7900> at usbus1, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=SAVE (100mA)
在 A 机器上 sudo pkg install nut
安装 NUT 软件。在 /etc/rc.conf 中配置自动启动:
# UPS configs of NUT master nut_enable="YES" nut_upslog_enable="YES" nut_upsmon_enable="YES"
在 A 机器上使用 sudo nut-scanner -U
扫描 UPS 设备。并将 UPS信息填入 /usr/local/etc/nut/ups.conf 中:
# ...
maxretry = 3
# ...
[nutdev1]
desc = "BK650M2-CH"
driver = "usbhid-ups"
port = "auto"
vendorid = "051D"
productid = "0002"
product = "Back-UPS BK650M2-CH FW:294803G -292804G"
serial = "xxxxxxxxxxxx"
vendor = "American Power Conversion"
# 有了上面的 port = "auto" 配置,upsd 可以自动找到对应的 UPS
# 所以这个指定 usb 端口的选项可以注释掉
#bus = "000"
# 这个参数设置了 NUT 从 USB 接口快速更新 (quick update) UPS 状态的时间间隔
# 默认值 2s,改成 6s 可以减轻通信压力,不过 2s 的频率也没什么压力
#pollinterval = 6
# pollfreq 是从 USB 接口全量更新 (full update) UPS 状态的间隔,默认 30s
#pollfreq = 30
# ignorelb 表示让 NUT 忽略 UPS 传来的低电量状态标志,
# 有些 UPS 检测到市电输入消失就会立即发送低电量状态信号到 NUT,
# 而 NUT 检测到低电量状态信号就立即触发关机操作。
# 设置 ignorelb 忽略低电量信号之后,还需要设置 battery.charge.low
# 和 battery.runtime.low 两个变量来定义低电量状态什么时候被触发。
# -1 表示不使用这个变量。
# 变量的值满足下面条件之一就触发关机操作:
# battery.charge < battery.charge.low
# battery.runtime < battery.runtime.low
ignorelb
# 表示不使用 剩余最大可运行时间 来进行低电量状态判定
override.battery.runtime.low = -1
# 表示当 UPS 电量低于 40% 时触发低电量状态,NUT 收到低电量状态信号后关机。
# 选择 40% 是为了方便定期放电到 50% 保持电池健康,但又不让机器关机。
# 40% 的电量足够让机器在断电时有足够电量撑到所有 slave 关机后自己再关机。
override.battery.charge.low = 40
A 机器 /usr/local/etc/nut/nut.conf 中的配置:
# ...
MODE=netserver
# 只有一台需要使用 UPS 的设备的话,用 standalone 就行
# 但这里写 netserver 的原因是后续可能有其他的设备使用这个 UPS
# 直接写成 netserver 免得后面再改模式
A 机器 /usr/local/etc/nut/upsd.conf 中的配置:
# ...
# 这个文件里的多数配置保持默认就够用。
# 如果机器有多个 IP 就读一下 54 到 72 行的 LISTEN 部分,然后自己配置允许哪些 IP 访问 nut-server
# 或者直接写成下面这样,允许从本机的所有 IP 发来的数据包访问 nut-server
LISTEN 0.0.0.0 3493
A 机器 /usr/local/etc/nut/upsd.users 中的配置:
# ...
# 这个给 nut-server 所在主机上的 client
# [] 中的是 username
# upsmon 值选 slave 表示这台主机在收到关机信号以后立刻关机
# 选 master 表示这台主机最后关机 (等 slave 全部关机以后再关机)
[admin]
password = adminpwd
upsmon master
# 这是 client 要使用的角色,由于需求简单,一个角色就够用了
[slave1]
password = slave1pwd
upsmon slave
在 A 机器上启动 upsd, upslog:
sudo /usr/local/etc/rc.d/nut restart
sudo /usr/local/etc/rc.d/nut_upslog restart
另外,如果第一条命令报找不到 HID 设备:
libusb1: Could not open any HID devices: no USB buses found
但 sudo nut-scanner -U
确实可以扫描到 UPS 设备。那么此时需要重启系统即可解决这个问题,目前问题的原因还不清楚,推测可能是有内核模块没加载。
在 A 机器上执行 sudo upsc nutdev1
以检测 upsd 是否工作正常,输出类似这样的:
battery.charge: 97 battery.charge.low: 60 battery.mfr.date: 2001/01/01 battery.runtime: 3116 battery.runtime.low: -1 battery.type: PbAc battery.voltage: 13.5 battery.voltage.nominal: 12.0 device.mfr: American Power Conversion device.model: Back-UPS BK650M2-CH device.serial: xxxxxxxxxxxx device.type: ups driver.flag.ignorelb: enabled driver.name: usbhid-ups driver.parameter.bus: 000 driver.parameter.pollfreq: 30 driver.parameter.pollinterval: 2 driver.parameter.port: auto driver.parameter.product: Back-UPS BK650M2-CH FW:294803G -292804G driver.parameter.productid: 0002 driver.parameter.serial: xxxxxxxxxxxx driver.parameter.synchronous: auto driver.parameter.vendor: American Power Conversion driver.parameter.vendorid: 051D driver.version: 2.8.0 driver.version.data: APC HID 0.98 driver.version.internal: 0.47 driver.version.usb: libusb-1.0.0 (API: 0x1000102) input.sensitivity: low input.transfer.high: 278 input.transfer.low: 160 input.voltage: 239.0 input.voltage.nominal: 220 ups.beeper.status: disabled ups.delay.shutdown: 20 ups.firmware: 294803G -292804G ups.load: 12 ups.mfr: American Power Conversion ups.mfr.date: 2023/05/22 ups.model: Back-UPS BK650M2-CH ups.productid: 0002 ups.realpower.nominal: 390 ups.serial: xxxxxxxxxxxx ups.status: OL CHRG ups.test.result: No test initiated ups.timer.reboot: 0 ups.timer.shutdown: -1 ups.vendorid: 051d
A 机器 /usr/local/etc/nut/upsmon.conf 中的配置:
# ...
# 这里写 NUT 要监控些什么
# 主要读一读 32 到 111 行
# 写下面的配置就够了
MONITOR nutdev1@localhost 1 admin adminpwd master
# MONITOR 表示命令
# nutdev1 是 ups.conf 里定义的配置名称
# @ 是连接符,localhost 是主机地址
# 1 表示这台机器只有一个电源
# (条件宽裕的机房里每个服务器配 2 个 UPS,有 2 个 UPS 的机器配置信息更多,这里略过)
# admin adminpwd master 是 upsd.conf 里定义的账户、密码与对应角色
最后在 A 机器启动 client 进程 upsmon sudo /usr/local/etc/rc.d/nut_upsmon restart
至此 A 机器配置完成。
可以先在 A 机器上拔掉 UPS 电源测试一下。如果一切正常,拔掉 UPS 电源之后,机器会继续运行,直到 UPS 剩余电量到 60% 的时候,机器关机。
实际使用时可能在连到 A 机器的 SSH 里看到 UPS 剩余电量低于 60% 的时候才关机 (电量剩余 59% 的时候才开始关机,完成关机以后剩余电量可能小于 59%),这是正常的:
- 因为 /usr/local/etc/nut/nut.conf 中的 mode 用的是
nutserver
,所以当触发关机指令时,nut-server 所在的 master 节点会先通知各个 slave 节点关机,然后自己再关机。这个过程中:- 如果这个时候刚好 slave 掉线了,那么 nut-server 在等待
DEADTIME
定义的秒数之后才放弃连接这个 slave。 - 如果 slave 全部都收到关机信号了。那么 master 还会继续等待
FINALDELAY
定义的秒数之后才对本机发出关机指令。 - (更多配置在 /usr/local/etc/nut/upsmon.conf 中,可自行查看)
- 如果这个时候刚好 slave 掉线了,那么 nut-server 在等待
- 如果 master 节点跑的服务比较复杂,那么关机时停止各项服务也需要时间。比如,虚拟化服务器关机前要迁移或关闭其上的虚拟机,这个过程很花时间。
根据配置目标,B 主机中需要修改的文件有:
- nut.conf
- upsmon.conf
在 B 机器上 sudo pkg install nut
安装 NUT 软件。在 /etc/rc.conf 中配置自动启动:
# UPS configs of NUT slave #nut_upslog_enable="YES" nut_upsmon_enable="YES"
B 机器 /usr/local/etc/nut/nut.conf 中的配置:
# ...
MODE=netclient
B 机器 /usr/local/etc/nut/upsmon.conf 中的配置:
# ...
# 这里写 NUT 要监控些什么
# 主要读一读 32 到 111 行
# 写下面的配置就够了
MONITOR nutdev1@<host-A> 1 slave1 slave1pwd slave
# <host-A> 可以是 IP, FQDN 或者主机名
# 这里的 slave1 slave1pwd slave 是 A 机器上的
# /usr/local/etc/nut/upsd.users 中定义的用户配置
最后在 B 机器执行 sudo /usr/local/etc/rc.d/nut_upsmon restart
启动 client。
至此 B 机器配置完成。